Skip to content

feat(design-system): ratchet raw scale literals and gate type-step selection (#262 parts 2 and 3) - #1780

Merged
BigSimmo merged 11 commits into
mainfrom
claude/m3-token-debt-262-261
Aug 9, 2026
Merged

feat(design-system): ratchet raw scale literals and gate type-step selection (#262 parts 2 and 3)#1780
BigSimmo merged 11 commits into
mainfrom
claude/m3-token-debt-262-261

Conversation

@BigSimmo

@BigSimmo BigSimmo commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

Three separately revertible commits, plus a docs correction. Two ledger rows were closed as already shipped, and #262 parts 2 and 3 add gates. No live-look change: the diff touches only scripts/, tests/ and docs/.

  • Close #218 and #270, both shipped before this branch existed. Both rows were still open while their work was live on main, which had already scoped a third session from them. #218 (cn() lacks tailwind-merge) shipped in feat(design-system): give cn() tailwind-merge (#218) #1678, aeba5a254: src/components/ui-primitives.tsx:37 is twMergeClinical(...) rather than a plain join, package.json carries tailwind-merge ^3.6.0, and src/lib/tailwind-merge.ts declares the repo's @theme scales to twMerge. #270 (declare the tap spacing token) shipped in fix(tailwind-merge): declare the tap spacing token now its blocker is disproved #1738, 80cf78139, confirmed an ancestor of origin/main: "tap" is present in CLINICAL_TWMERGE_THEME.spacing, and tests/tailwind-merge-config.test.ts was inverted rather than deleted, so the merge behaviour is asserted rather than pinned out. Verified in source, not inferred from the handover.

  • #262 part 3 — ratchet raw padding, radius and line-height literals. The contract ratcheted colour, shadow, tap and tracking but not spacing, radius or line-height, so a value could bypass the scale as a bare literal in either a class or a stylesheet and nothing objected. Adds rawPaddingLiterals (67), rawRadiusLiterals (24) and rawLineHeightLiterals (3), each ratcheted per path and each covering both the class utilities and the CSS declarations, the way the colour and legacy-shadow metrics already do. The exemption is deliberately "contains no CSS function" rather than the narrower (?!var\() the tracking rule uses, because production ships pb-[env(safe-area-inset-bottom)], pt-[max(0.75rem,var(--safe-area-top))], pt-[clamp(1.5rem,5vh,3rem)] and pb-[calc(7rem+env(safe-area-inset-bottom))] — all computed from the viewport or the safe-area inset, none of them spellable as a scale step, and a var(-only lookahead would have flagged every one.

  • #262 part 2 — gate type-step selection on the decidable half. check:type-scale already blocks arbitrary text-[12px]; nothing stopped the scale itself growing a step no surface ever picks. Whether a heading should have chosen text-sm over text-sm-minus is not mechanically decidable and this does not pretend otherwise, but a step declared and consumed by nobody is. There is one today: --text-2xl-compact has zero consumers — no utility use, no var() use — while the next-rarest step, text-hero, has one real consumer. The analyzer reports every bare text-<name> it sees and does not decide which names are steps; the checker intersects that against the @theme block it parses from globals.css, so the scale is never written down twice. Retiring the dead step edits @theme, so it is carried as a documented exemption tracked by #295 rather than bundled into a gate change, and the exemption cannot rot: the build also fails if an exempted step stops being declared or gains a consumer.

  • Correct GATES.md. Its §1 is the list of what actually runs, and this area's recurring failure is that list lagging the code — four of #264's six prohibitions were already gated while it said planned. Records the new ratchets and the step rule, and rewrites the callout that claimed a step-selection lint "does not exist".

Verification

  • npm run check:design-system-contract — passes, Scale ratchets: raw padding literals 67; raw radius literals 24; raw line-height literals 3.
  • npm run check:icon-scale✓ icon-scale: no retired 4.5 (18px) half-step icon sizes in src. Run explicitly because it is not part of the contract check.
  • npm run check:type-scale✓ type-scale: no arbitrary text-[<n>px|rem|em] font sizes in src.
  • npm run check:outstanding-issues294 rows (145 open, 149 archived), unique ids, next-id=297 above the highest, no merge driver, no ids deleted from base 7aaf9349cd64.
  • npx vitest run tests/design-system-contract-utils.test.tsTests 31 passed (31) (28 pre-existing plus 3 added).
  • npm run format:check — whole tree, All matched files use Prettier code style!
  • npm run verify:cheap — exit 1, from five pre-existing failing files, none of them this branch's. See below.
  • npm run verify:ui — not run, and not applicable: no UI, routing, styling, reduced-motion or forced-colors behaviour changed. The diff contains no src/ file.

Every baseline entry was verified in source before pinning. All 94 findings were checked to be present at their cited line — a mechanical whole-population check, not a sample. Zero false positives, which is the point of putting these in the AST class-root and postcss declaration passes rather than a text scan.

The baseline change is additive. Every one of the fifteen pre-existing metrics and every pre-existing debtByPath entry is byte-identical; only the three new keys were added. Asserted before the file was written, not after.

Mutation-tested rather than assumed — a ratchet never seen to fail is not a gate. Part 3, class side, in a file with no prior debt in any of the three metrics:

- rawPaddingLiterals increased from 67 to 68
- rawPaddingLiterals at src/components/ui-primitives.tsx increased from 0 to 1
- rawRadiusLiterals increased from 24 to 25
- rawRadiusLiterals at src/components/ui-primitives.tsx increased from 0 to 1
- rawLineHeightLiterals increased from 3 to 4
- rawLineHeightLiterals at src/components/ui-primitives.tsx increased from 0 to 1

The CSS half fails the same way against a probe stylesheet. Both probes also carried the sanctioned computed forms, and each count rose by exactly one, so the exemptions are proved by the same runs rather than argued.

Part 2 was mutation-tested three ways: dropping the exemption, adding a brand-new unused step, and giving the exempted step a consumer.

- type steps are declared in globals.css @theme but no production surface selects them: --text-2xl-compact (text-2xl-compact). Retire the step or use it; do not leave the scale carrying a step nobody picks.
- --text-2xl-compact is exempted as unused but production now selects text-2xl-compact — drop the exemption

On the verify:cheap exit code. It exits 1 on five pre-existing failing files: Test Files 5 failed | 541 passed (546), Tests 9 failed | 5857 passed | 14 skipped. That run predates merging main. After merging, three of the five pass — #1771 fixed the runtime/setup ones, and tests/worker-observability.test.ts is the known order-dependent case that passes in a small batch. The two that remain, tests/pr-handoff-stop.test.ts and tests/document-viewer-page-virtualization.dom.test.tsx, cannot be caused by this branch: neither references anything in the diff, and both the test files and their subjects (.claude/hooks/pr-handoff-stop.sh, the document viewer) are byte-identical to origin/main. The virtualization file also failed a different test name across two runs, which is a timing flake.

Risk and rollout

  • Risk: low. Additive gates only. The two ratchets are ceilings, so existing debt passes and only growth fails; the step rule is a hard rule but is green at this HEAD with one documented exemption. No src/ file changes, so nothing rendered moves.
  • Rollback: git revert the individual commit. The three are independent — the two gate commits touch different logic in the same two files, and the docs commit touches neither. Reverting part 3 alone requires also dropping its three baseline keys, since the checker asserts every metric has a baseline entry.
  • Provider or production effects: None. No OpenAI, Supabase, Railway or hosted-CI call was made.

Notes

Three figures for the same measurement were in circulation for #262, and none of the documents agreed. All three are now reproduced, so the next reader does not have to re-derive them:

  • "1 318 call sites" (the row, and GATES.md) is a repo-wide grep including src/app/mockups/**, which every one of these gates excludes. It is 1 360 today.
  • "733" counts the @theme declarations and doc comments as if they were usages.
  • 705 is the actual production consumer count: text-2xs 421, sm-minus 160, base-minus 57, 3xs 42, 2xl-minus 9, 3xl-minus 9, lg-minus 6, hero 1, 2xl-compact 0. There are nine non-standard steps, not eight.

One correction that matters for #262 part 1, which is not in this PR: its scope is 93 var(--shadow-tight) sites across 50 files, not 94 across 51. The 94th is a doc comment at src/lib/tailwind-merge.ts:40, which the contract check strips. Clearing them therefore leaves 127 aliases from the other six tokens, not 126, and legacyShadowAliases: 0 remains the wrong success criterion.

Two ledger rows were opened from findings made while measuring: #295 (retire --text-2xl-compact) and #296 (src/components/route-error-boundary.tsx:106 renders text-green-600, the only chromatic Tailwind palette utility in production — it escapes every colour gate legitimately, because LEGACY_PALETTE_UTILITY deliberately matches only the neutral ramps).

Auto-merge deliberately not armed: this changes a gate.

BigSimmo and others added 6 commits August 9, 2026 14:58
Both rows were still open in docs/outstanding-issues.md while their work was
already live on main, which had scoped a third session from them.

#218 (cn() lacks tailwind-merge) shipped in PR #1678, aeba5a2.
src/components/ui-primitives.tsx:37 is twMergeClinical(...) rather than a plain
join, package.json carries tailwind-merge ^3.6.0, and src/lib/tailwind-merge.ts
declares the repo's @theme scales to twMerge.

#270 (declare the tap spacing token) shipped in PR #1738, 80cf781, an ancestor
of origin/main. "tap" is present in CLINICAL_TWMERGE_THEME.spacing, and
tests/tailwind-merge-config.test.ts was inverted rather than deleted so the
merge behaviour is now asserted rather than pinned out.

Verified in source at origin/main 7aaf934, not inferred from the handover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rals (#262 part 3)

The design-system contract ratcheted colour, shadow, tap and tracking but not
spacing, radius or line-height, so a value could bypass the scale as a bare
literal in either a class or a stylesheet and nothing objected.

Adds three per-path ratchets, covering both halves the way the colour and
legacy-shadow metrics already do:

  rawPaddingLiterals      67  (17 CSS declarations, 50 class utilities)
  rawRadiusLiterals       24  (22 CSS declarations, 2 class utilities)
  rawLineHeightLiterals    3  (3 CSS declarations)

The exemption is deliberately "contains no CSS function", not the narrower
`(?!var\()` the tracking rule uses. Padding is not only ever a token or a
literal: production ships pb-[env(safe-area-inset-bottom)],
pt-[max(0.75rem,var(--safe-area-top))], pt-[clamp(1.5rem,5vh,3rem)] and
pb-[calc(7rem+env(safe-area-inset-bottom))]. Those are computed from the
viewport or the safe-area inset, cannot be spelled as a scale step, and a
`var(`-only lookahead would have flagged every one of them. On the CSS side,
zero in any unit, the CSS-wide keywords and custom-property declarations (the
token definitions themselves) are exempt for the same reason.

Every one of the 94 baseline entries was verified present at its cited line
before pinning, and the baseline change is additive: all fifteen pre-existing
metrics and every pre-existing debtByPath entry are byte-identical.

Mutation-tested rather than assumed. Class side, in a file with no prior debt:

  - rawPaddingLiterals increased from 67 to 68
  - rawPaddingLiterals at src/components/ui-primitives.tsx increased from 0 to 1
  - rawRadiusLiterals increased from 24 to 25
  - rawRadiusLiterals at src/components/ui-primitives.tsx increased from 0 to 1
  - rawLineHeightLiterals increased from 3 to 4
  - rawLineHeightLiterals at src/components/ui-primitives.tsx increased from 0 to 1

The CSS half fails the same way. Both probes also carried the sanctioned
computed forms, and each count rose by exactly one, so the exemptions are
proved by the same runs rather than argued.

No new npm script: the metrics live inside check:design-system-contract, so
docs:check-inventory and check:gate-manifest are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…262 part 2)

check:type-scale blocks arbitrary text-[12px] values. Nothing has stopped the
scale itself growing a step no surface ever picks, which is the drift that
makes a wrong selection possible in the first place.

Whether a heading should have chosen text-sm over text-sm-minus is not
mechanically decidable, and this does not pretend otherwise. A step that is
declared and consumed by nobody is decidable, and there is one today:
--text-2xl-compact (globals.css:112) has zero consumers -- no utility use, no
var() use -- while the next-rarest step, text-hero, has one real consumer.

The analyzer reports every bare text-<name> it sees and does not decide which
names are steps; the checker intersects that against the @theme block it parses
from globals.css. So the scale is never written down twice, and a step added to
globals.css is covered without touching this gate.

Retiring the dead step edits @theme, so it gets its own revertible PR rather
than riding along here: it is carried in UNUSED_TYPE_STEP_EXEMPTIONS and
tracked as docs/outstanding-issues.md #295. The exemption cannot rot silently --
the gate also fails if an exempted step stops being declared or gains a
consumer.

Mutation-tested, three ways:

  - type steps are declared in globals.css @theme but no production surface
    selects them: --text-2xl-compact (text-2xl-compact). Retire the step or use
    it; do not leave the scale carrying a step nobody picks.
  - (a newly added --text-probe-step fails identically, so this catches future
    drift rather than only today's known case)
  - --text-2xl-compact is exempted as unused but production now selects
    text-2xl-compact -- drop the exemption

Measurement note, since three different figures were in circulation for this
row: the "1318 sites" is a repo-wide grep INCLUDING mockups, which the gate
excludes (1360 at this HEAD). Production consumers of the nine non-standard
steps total 705 -- text-2xs 421, sm-minus 160, base-minus 57, 3xs 42,
2xl-minus 9, 3xl-minus 9, lg-minus 6, hero 1, 2xl-compact 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GATES.md's own §1 is the list of what actually runs, and this series' recurring
failure is that list lagging the code: four of #264's six prohibitions were
already gated while it said "planned".

Records the padding/radius/line-height ratchets and the type-step selection
rule in the contract row, and rewrites the type-scale callout, which claimed a
step-selection lint "does not exist". The decidable half now ships; the half
that asks whether text-sm-minus was the right pick over text-sm still does not,
and cannot.

Also corrects the "1 318 call sites" figure quoted there. It was a repo-wide
grep including src/app/mockups/**, which every one of these gates excludes
(1 360 at 7aaf934). Production consumers total 705, and there are nine
non-standard steps, not eight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 12 minutes

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 722107ec-c30a-4f42-b75d-880e4bd2951e

📥 Commits

Reviewing files that changed from the base of the PR and between 7a4ffe2 and 78facd7.

📒 Files selected for processing (7)
  • docs/branch-review-ledger.md
  • docs/design-system/GATES.md
  • docs/outstanding-issues.md
  • scripts/check-design-system-contract.mjs
  • scripts/design-system-contract-baseline.json
  • scripts/design-system-contract-utils.mjs
  • tests/design-system-contract-utils.test.ts

Comment @coderabbitai help to get the list of available commands.

@supabase

supabase Bot commented Aug 9, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project sjrfecxgysukkwxsowpy because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c6e1fe7fc4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/design-system-contract-utils.mjs
Comment thread scripts/check-design-system-contract.mjs Outdated
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Resolve docs/outstanding-issues.md by keeping main's #295/#296/#252
archive updates, re-closing #218/#270 from this PR, and renumbering the
text-2xl-compact retirement task to #297 to avoid the id collision.

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds design-system gates for raw scale literals and unused typography steps while updating related debt tracking and documentation.

Changes:

  • Ratchets raw padding, radius, and line-height literals.
  • Detects declared but unused typography steps.
  • Archives completed issues and documents follow-up debt.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
scripts/design-system-contract-utils.mjs Adds scale-literal and type-step analysis.
scripts/check-design-system-contract.mjs Enforces new ratchets and unused-step rules.
scripts/design-system-contract-baseline.json Records current per-path debt.
tests/design-system-contract-utils.test.ts Tests the new analyzers.
docs/design-system/GATES.md Documents active design-system gates.
docs/outstanding-issues.md Archives shipped work and adds follow-ups.
docs/branch-review-ledger.md Records the PR review state.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/design-system-contract-utils.mjs
Comment thread scripts/check-design-system-contract.mjs Outdated
Comment thread scripts/design-system-contract-utils.mjs Outdated
cursoragent and others added 2 commits August 9, 2026 09:53
Cover Tailwind arbitrary-property forms and modern CSS zero units in the
raw scale ratchets, and validate unused-step exemptions against the same
class-or-CSS consumer predicate used for ordinary steps.

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Clear behind-but-clean drift after main advanced (#1775); merge-tree was clean.

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
@BigSimmo

BigSimmo commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

PR #1780 review-and-fix summary

Tip: 78facd77aff2b8c3ec0901f40bb4c486af3ce483 on claude/m3-token-debt-262-261
Base: origin/main @7a4ffe2f (DSM search header). Ahead 11 / behind 0.

Sync / merge-tree

  • Started from GitHub head 1671c167 (0 behind at snapshot).
  • One sync after main advanced with #1775: merge-tree stayed clean before and after (git merge-tree --write-tree exit 0).
  • GitHub PR mergeability now success on this tip. mergeable_state: blocked is waiting on in-flight required checks, not a content conflict.
  • Auto-merge: not armed.

Review

  • Bugbot: no bugs.
  • High-confidence delta risks only (gate scripts / tests / docs). No RAG / ranking / src/ product behaviour.
  • PR body note still says retirement tracked as #295; the live exemption and /issues row are #297 (informational only).

Fixed (P2, pushed in 08ac250a, carried through the main sync)

  1. Arbitrary-property ratchet bypass[padding:…] / [border-radius:…] / [line-height:…] now share the raw-literal predicate with named utilities.
  2. Unused-step exemption anti-rot — one typeStepIsConsumed predicate for class utilities and direct var(--text-*) across walked production sources (CSS via declarations; TS/TSX comment-stripped).
  3. Modern CSS zero units0dvh / 0svw / 0cqw / 0lh etc. exempt as designed.

All five review threads replied with fixed-head:08ac250a… markers and resolved.

Dispositioned

  • None left open. No human-needed P0/P1.

Required CI (this tip)

  • Selected: Static PR / Unit coverage / Safety — in progress after the sync push.
  • Production UI / Build / Migration replay — skipped (no src/ product change; expected).
  • Advisory ignored.
  • Prior tip 1671c167 had required checks green before the fix push.

Local gates (decisive lines)

  • npx vitest run tests/design-system-contract-utils.test.tsTests 32 passed (32)
  • npm run check:design-system-contractDesign-system contract passed / Scale ratchets: raw padding literals 67; raw radius literals 24; raw line-height literals 3.
  • CSS-exemption mutation → contract failed on stale exemption, then restored green
  • npm run verify:cheapTest Files 549 passed (549) / Tests 5933 passed | 4 skipped
  • verify:pr-local: lint/typecheck/docs stages completed; full npm run test re-run 549 passed; check:rag:fixturesOffline RAG fixture and manifest validation passed (36 golden cases, 23 suites). One pre-existing design-system-adoption 30s timeout under suite load; isolated retest 51 passed.
  • No provider / release / live / UI gates (out of scope; no src/ behaviour change).

Ledger

  • Pushed review rows under scope PR #1780 review-and-fix (including sync note at fe75e6ac).
  • Tip-align row for 78facd77 left local and unpushed to avoid a ledger-only tip.

Residual risk

  • Low: gate coverage for less-common arbitrary-property spellings beyond padding/radius/line-height is unchanged.
  • #297 still owns retiring --text-2xl-compact.

Merge left to you.

@BigSimmo
BigSimmo enabled auto-merge (squash) August 9, 2026 09:57
@BigSimmo
BigSimmo merged commit ef9bb51 into main Aug 9, 2026
24 checks passed
@BigSimmo
BigSimmo deleted the claude/m3-token-debt-262-261 branch August 9, 2026 10:00
@BigSimmo BigSimmo mentioned this pull request Aug 9, 2026
14 tasks
BigSimmo added a commit that referenced this pull request Aug 9, 2026
…1786)

* feat(design-system): build ErrorState, the gate with nothing behind it

GATES.md §3 lists the prohibition "Render '0 matches' after a failed
request" with the gate "ErrorState adoption + check", status planned.
Measured at origin/main 199b303, ErrorState existed nowhere in src or
tests — only in COMPONENTS.md, GATES.md and SPEC.md. This builds it.

The invariant is clinical, not cosmetic. A search that failed has no count
to report, so reporting zero is a false statement about the corpus: on the
services page "0 matches" asserts there are no crisis services when the
search never ran, and on favourites it reads as "you have saved nothing"
rather than "we could not load them". COMPONENTS.md:322 draws the same line
from the other side — "no result count is available" is not a MissingValue.

The component therefore takes no count and no children. There is no prop
through which a number can arrive, and the generated dtsPropsFor entry now
records that as the published API. The one remaining route, a caller writing
a count into title or body, is covered by a development-time tripwire that
matches a figure against a counted noun ("0 matches", "no results") so an
error code or a duration does not trip it. It warns and never throws: on the
one screen already reporting a failure, a thrown error is a blank page.

Requirements came from the surfaces that hand-roll this guard today. Three
do, and their comments state the rule outright:
search-results-header-band.tsx:210 ("no number may reach the DOM"),
services-navigator-page.tsx:634 ("a blocked registry must not reach the band
as '0 matches'") and favourites-command-library-page.tsx:1182. They are
correct, just not shared; converting them is a live-look change and
deliberately not here.

Three further sites carried into this task as hand-rolled guards are not
that, measured at this HEAD, and are recorded so the next reader does not
convert them: differentials-home.tsx:716,729 renders "0 matches"/"No
matches" when sourcesChecked is true, i.e. a legitimate zero after a search
that SUCCEEDED; specifiers-home-page.tsx:211 is a comment about not showing
a stale zero above real catalogue results, and lives in
src/components/specifiers/, not clinical-dashboard/; document-search-results
gates on recordStatus for loading, not for a failed count.

Registered per gates 11 and 12: source, design-sync export, preview, prop
contract, publication test entry, behavioural DOM tests, adoption-contract
family, and both generators regenerated (54 components, 59 roots).

Uses the shared floatingControl recipe rather than a hand-rolled control, so
the tap floor, focus ring and forced-colors border come from one owner. Adds
zero arbitrary padding/gap/radius/line-height, so the ratchet landed in the
previous commit is unaffected and the two stay separately revertible.

Scope: ErrorState only. OfflineState, PermissionDeniedState, NotFoundState
and UnavailableState share the pattern but have no gate pointing at them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(design-system): ratchet raw gap literals, the family #1780 left uncovered

#1780 landed rawPaddingLiterals, rawRadiusLiterals and rawLineHeightLiterals
for #262 part 3. Gap was the one remaining spacing surface a hand-picked
value could hide in: gap-[9px] and `gap: 18px` were counted by no ratchet at
all. This adds rawGapLiterals on that commit's own predicate and wiring.

Measured against origin/main ef9bb51: 34 sites across 9 files — 21 Tailwind
utilities, every one under src/components/therapy-compass/, plus 13 CSS
declarations in globals.css that a utility-only scan misses. Covering both
spellings is the same reason #1780 counts both: otherwise a literal escapes by
moving from a class into globals.css.

Kept as its own metric rather than folded into rawPaddingLiterals so the
therapy-compass cleanup can be paid down and re-pinned independently of the
padding debt, which is spread across fifteen unrelated files.

Reuses RAW_LITERAL_VALUE unchanged, so a value containing a CSS function
(env(, clamp(, max(, calc() stays a sanctioned computed form and is exempt.

Also corrects the §3 prohibition row, which #1780 left reading
"implemented-partial (colour/shadow/tap literals only)" and which named none
of the metrics it had just shipped. A row that understates shipped work is
what sends the next session to rebuild it — this change was itself started as
a duplicate of #262 part 3 for exactly that reason.

Mutation-verified in both halves, each naming the metric and the path:
a gap-[3px] utility gives "rawGapLiterals increased from 34 to 35" plus
"at src/components/ui/missing-value.tsx increased from 0 to 1"; a
`gap: 19px` declaration gives the same total plus "at src/app/globals.css
increased from 13 to 14".

Baseline diff is additive only. legacyShadowAliases measures 218 against its
pinned 220 on main; that slack is left exactly as found.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(issues): capture the ErrorState and duplicate-work follow-ups

Four rows (#298-#301), added with npm run issues:add. Ids 295-297 were
claimed by main while this branch was open, which is ledger #156's
read-modify-write race behaving exactly as recorded.

- #298 (P2 task) the ErrorState enforcement check. GATES.md still reads
  "planned" for the 0-matches prohibition and nothing in scripts/ or
  eslint-rules/ references ErrorState, so the component exists but is not
  required. Deliberately not flipped to implemented.
- #299 (P3 task) adopting ErrorState at the three surfaces that genuinely
  hand-roll the guard. Live-look change, downstream of the redesign.
- #300 (P2 issue) three sites miscarried into M4 as guards that are not,
  so the next reader does not convert them. differentials-home renders its
  zero after a search that SUCCEEDED.
- #301 (P3 issue) two sessions built #262 part 3 in parallel because the
  §3 row understated what had shipped. Proposes asserting that every
  baseline metric key appears in GATES.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(design-system): restrict the ErrorState copy tripwire to development

Codex review on #1786 (P2). The doc comment described the tripwire as
development-only, but the emitter only silenced NODE_ENV === "test", so a
production caller supplying count-bearing title/body copy had the full
caller-provided string written to console.warn. On a clinical surface that
copy can quote the query — "0 results for <query>" — which turns a copy
defect into a disclosure risk. Nobody reads a production browser console
for design-system warnings, so the emit is now development-only and an
unset NODE_ENV is treated as production: fail quiet.

The gate is an exported predicate rather than an inline comparison because
an inline comparison is untestable here. Vite statically replaces
process.env.NODE_ENV inside src/ modules, so under Vitest the check
compiles to `"test" === "development"` and no stubEnv can move it. The
review asked for a production console-spy check; written that way it would
have passed while proving nothing, staying silent for the wrong reason and
continuing to pass even if the guard were deleted. shouldEmitErrorStateDiagnostic
is asserted directly instead — development true; production, test and unset
false — with the console spy kept alongside as the weaker check that catches
an emitter which warns unconditionally.

Verified: typecheck 0 errors, lint 0, format:check 0,
check:design-system-contract 0, Tests 70 passed (70) across
error-state.dom and design-sync-visual-exports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
BigSimmo added a commit that referenced this pull request Aug 14, 2026
…bt rows (#1942)

* refactor(tokens): re-land the --shadow-tight retirement onto --e1

PR #1803 retired the --shadow-tight role alias in favour of the --e1
elevation tier across 49 files and squash-merged as 9d8370a on
2026-08-10. The acf78bf merge on 2026-08-11 silently reverted it, along
with six other PRs. This re-applies the retirement against current main:
130 call sites across 67 files, plus both declarations.

The alias was a pure pass-through -- `--shadow-tight: var(--e1)` in the
light and dark role blocks -- so the substitution is value-preserving.
Confirmed for forced-colors too rather than assumed: the
`@media (forced-colors: active)` block scopes `:root, .dark`, the same
`html` element the alias is declared on, so `--shadow-tight` already
resolved through the flattened `--e1: none` there. The .ckb-v2
redeclaration hazard does not bite for the same reason -- .ckb-v2 sits on
<html> and .ckb-v2.ckb-v2 outspecifies :root, so both spellings
substitute against the winning v2 tier.

Two comments survived acf78bf while the code they describe did not: the
globals.css note that "the resting-hairline role is gone", and the token
test's "unlike the --shadow-tight assertion above". Both are accurate
again.

The token contract test now sweeps the tracked src tree for both
spellings (declaration and var() consumer) instead of only asserting the
declaration. A declaration-only check would have caught this particular
revert, but only because the declarations happened to come back with the
call sites; sweeping both makes the gate independent of which half of a
bad merge lands. Mutation-verified in both directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XsBPUoxpsvTwvFMJzrNoyz

* chore(design-system): re-pin the contract ratchets to their measured values

`scripts/design-system-contract-baseline.json` is a ceiling, so paying
debt down leaves silent headroom behind. Ledger #302 records that
pattern: legacyShadowAliases was pinned at 220 against a measured 193,
27 units of unguarded slack, up from 3 units on 2026-08-10.

With the previous commit's --shadow-tight retirement applied the gap is
wider still -- 220 pinned against 119 measured -- because the reland pays
down the debt the acf78bf revert had re-hidden. Four other ratchets had
accumulated slack from unrelated work in the same window.

  legacyShadowAliases        220 -> 119
  edgeOwnershipConflicts      27 -> 25
  rawPaddingLiterals          67 -> 63
  rawGapLiterals              34 -> 32
  layoutTransitionExceptions  12 -> 11

Regenerated with --print-debt-baseline rather than hand-edited, so the
per-path debtByPath counts move with the totals -- those are what
findDebtPathRegressions compares, and the retirement moved them
wholesale. Every metric in the diff decreases; nothing is absorbed
upward.

This is not the baseline refresh #262 warns against. That stop rule
forbids refreshing to hide the movement; this pins the movement in so it
cannot silently drift back a second time.

Mutation-verified: reintroducing one alias in button.tsx now fails at
both the total (119 -> 120) and the per-path level. Under the old 220
ceiling the same addition passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XsBPUoxpsvTwvFMJzrNoyz

* refactor(tokens): hold the search-band count bubble in a spacing token

The active-filter badge sized itself with a raw `h-[1.0625rem]
min-w-[1.0625rem]` pair. Ledger #275 tracks that value as leaked debt:
it had reached five files, so the fix has always been to tokenise once
rather than edit a call site.

Re-measured on merged main, the badge role is down to a single call
site. #170's convergence landed in the meantime -- document-search-
results.tsx now renders the shared control and therapy-compass/
filter-sheet.tsx was deleted outright -- so the leak this row was
written about has already been reabsorbed by the extraction. Holding
the value in @theme is what stops it leaving again.

Two arbitrary values in the same component are deliberately left raw:

  pr-[0.6875rem] and min-[414px]:max-[429px] -- the repo defines no
  --breakpoint-* tokens at all, and eight peer sites use the same raw
  min-[]/max-[] form (359px, 389px, 414px). Naming one window while the
  peers stay raw is the same drift #275 warns about on another axis, and
  Tailwind named breakpoints would add variants across the whole utility
  surface. That belongs in a repo-wide decision, filed separately.

The three remaining 1.0625rem hits in mode-nav.tsx and nav-slot-ink.tsx
are NOT this token. They size <Icon> glyphs -- a 17px icon against a
12/14/16/20/24 --spacing-icon-* scale -- so folding them under a badge
token would merge two roles that only happen to share a number.
check:icon-scale deliberately does not flag arbitrary h-[Nrem], so they
are a real but separate finding, filed rather than guessed at.

The token is also registered in CLINICAL_TWMERGE_THEME.spacing, which
tests/tailwind-merge-config.test.ts asserts against the @theme block --
without it `cn()` cannot resolve a conflict on the new utility. Safe by
that file's own `tap` reasoning: the single call site is a static string
carrying no competing h-*/min-w-* class and never passes through `cn()`,
so there is no same-variant pair for declaration to hand to the later
class. The entry is protective for future use, not load-bearing today.

Value-preserving, and proven rather than inferred: compiling globals.css
through @tailwindcss/postcss emits
  .h-search-band-badge { height: var(--spacing-search-band-badge) }
  .min-w-search-band-badge { min-width: var(--spacing-search-band-badge) }
No ratchet moved, so the ceilings pinned in the previous commit still sit
at zero slack.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XsBPUoxpsvTwvFMJzrNoyz

* docs(design-system): close out DS Track A3 and refresh the stale gate rows

Track A3 is `#262`. Its three parts are now all settled, each checked
against code rather than against the row that describes it.

Part 1 is the --shadow-tight retirement re-landed earlier in this PR.
Part 3 shipped in PR #1780 per `#301`: rawPaddingLiterals,
rawRadiusLiterals and rawLineHeightLiterals are live baseline keys
enforced over both the class and CSS-declaration spellings, plus
rawGapLiterals beyond the original ask.

Part 2 needs no work, and that had already been adjudicated -- GATES.md
section 3 records it, which is why nothing here builds it. The decidable
half of step selection shipped on 9 Aug inside check:design-system-
contract: a declared @theme step no production surface selects fails the
build. The remaining half -- which existing step a component picks -- is
documented there as something "nothing mechanical can" gate, being a
judgement about the rendered design rather than a property of the source,
with a standing instruction not to duplicate the arbitrary-value check
check:type-scale already ships. Reading `#262` alone would have sent a
session to build it; that is the `#301` failure mode, so the closure
record says so explicitly.

Section 3's live status rows carried numbers this PR moved. `#301`'s
lesson is that a row understating shipped work is a duplicate-work
generator, so they are corrected in the same change:

  legacyShadowAliases        224 -> 119, and the alias is now retired
                             outright rather than "224 left to retire"
  edgeOwnershipConflicts      27 -> 25
  rawPaddingLiterals          67 -> 63
  rawGapLiterals              34 -> 32
  layoutTransitionExceptions  12 -> 11

Section 5 is left alone deliberately: it is a dated record measured
against 8db1e53, not a live status surface, and rewriting its figures
would destroy the provenance it exists to hold.

Ledger records are queued as immutable inbox requests: `#262`, `#302` and
`#275` closed; two carve-outs split out of `#275` filed as their own rows
(the repo-wide breakpoint-token decision, and three 17px mode-nav icon
glyphs that sit off the --spacing-icon-* scale with no gate covering
them). The queued re-land request 210e3db5 is cancelled rather than
reconciled -- its headline "67 files on main still use the retired alias"
is false as of this branch, so it would open a row wrong on arrival. The
request file and the cancellation both survive as provenance for the
acf78bf merge loss.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XsBPUoxpsvTwvFMJzrNoyz

* chore(ledger): record the design-token relands review

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XsBPUoxpsvTwvFMJzrNoyz

* fix(issues): retarget the reland record after main reconciled it mid-flight

CI failed `docs:check-links` on this branch with

  Error: cancel request 2e791c01... targets missing pending request
  210e3db5...

`check-docs-links.mjs` replays the inbox batch to resolve link targets, so
an unresolvable request fails it. The cause was a race, not a bad record:
PR #1936 reconciled 75 queued requests -- 210e3db5 among them -- while
this branch was already in flight. Reconciling moves the request file
into `docs/outstanding-issues-inbox/applied/` and allocates it a canonical
row, so by the time this branch merged main there was no pending request
left for the cancellation to name.

Cancelling was the right call against a pending request and is the wrong
one against a reconciled row. The cancel is dropped and replaced with a
`done` against `#319`, the row 210e3db5 became. That is also the better
record: the work is finished rather than withdrawn, so the ledger should
carry its outcome and its guard, which a cancellation would have thrown
away.

Also merges origin/main (this branch was 3 behind) and files two findings
the PR preflight surfaced, both deliberately not fixed here:

  - `check:medication-lexicon-report` has been failing on main for every
    local `verify:pr-local`, and no CI job runs it -- a grep over
    .github/workflows finds nothing. It is the last step of the local
    chain, so it fails preflights while CI stays green. The stale file is
    a clinical-facing generated document; regenerating it inside a
    CSS-token PR would bundle a clinical-risk artefact with unrelated
    chores.
  - Claude Code web containers can ship Node 22 with no node_modules,
    which fails `npm ci` on engine-strict before any repo script can run.

Re-verified after the merge: the tracked tree still holds zero
`--shadow-tight` references, and every pinned ratchet still measures
exactly its baseline, so the merge moved no metric and the pins stay
honest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XsBPUoxpsvTwvFMJzrNoyz

* docs(design): retire shadow-tight guidance

* docs(design): retire shadow-tight guidance

* docs(design): retire shadow-tight guidance

* docs(design): retire shadow-tight guidance

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants